#!/usr/bin/env python

#
# Nintendorks Forum iPhone Reformatter
# 2009 Ducson Nguyen
#

import os
import re
import sys
import time
import urllib
import logging
import traceback

from google.appengine.ext import db
from google.appengine.api import urlfetch
from google.appengine.ext import webapp
from google.appengine.api import memcache
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext.webapp import template

from BeautifulSoup import BeautifulSoup

# Message board-specific constants
#baseURL = 'http://nintendorkschatter.com/'
baseURL = 'http://plinkomedia.com/nintendorks/'
loginPath = 'ucp.php'
forumPath = 'viewforum.php'
threadPath = 'viewtopic.php'
replyPath = 'posting.php'
pmPath = 'ucp.php?i=pm'
threadsPerPage = 50
postsPerPage = 42

class NintendorksUser(db.Model):
  cookie = db.TextProperty()

# Presents main page. Redirects user to forum if they
# have logged in before.
class MainPage(webapp.RequestHandler):
  def get(self):
    # Load the forum if a user cookie has been set
    username = self.request.cookies.get('NintendorksUsername', '')
    if username != "":
      self.redirect('/index')

    else:
      # Write the submission form and the footer of the page
      path = os.path.join(os.path.dirname(__file__), 'home.html')
      self.response.out.write(template.render(path, None))

# Send login information to forum's login script. Saves cookie
# returned by the server into datastore for persistent logins.
class LoginHandler(webapp.RequestHandler):
  def post(self):
    # Set up POST parameters in dictionary form for logging in
    params = {'mode' : 'login',
              'username' : self.request.get('username'),
              'password' : self.request.get('password'),
              'autologin' : 'yes',
              'login' : 'Login'}
    
    # URL-encode parameters dictionary
    encodedParams = urllib.urlencode(params)
    
    try:
      # Send login parameters straight to login script
      result = urlfetch.fetch(
        url = baseURL + loginPath,
        payload = encodedParams, method=urlfetch.POST, follow_redirects=False, deadline=10)

      # Check if login was successful
      if result.content.find("successfully logged in") != -1:
        # Store returned cookie in datastore using username as key name;
        # key name is sanitized with "key:" in case the user starts their 
        # name with a number or other invalid character; also url-encoded
        encodedUsername = urllib.quote(self.request.get('username'))
     
        # Really bad cookie parsing: only take the data fields and ignore
        # expiration and path fields. This cookie will be used to
        # keep the user logged in
        cookie = result.headers['set-cookie']
     
        m = re.findall('(?<=phpbb3_d6lvl_u=)\w+', cookie)
        self.response.out.write(m)
        storedCookie = "phpbb3_d6lvl_u=" + m[3] + ";"
        m = re.findall('(?<=phpbb3_d6lvl_k=)\w+', cookie)
        self.response.out.write(m)
        storedCookie += "phpbb3_d6lvl_k=" + m[0] + ";"      
        m = re.findall('(?<=phpbb3_d6lvl_sid=)\w+', cookie)
        self.response.out.write(m)
        storedCookie += "phpbb3_d6lvl_sid=" + m[3] + ";"
    
        # Store login cookie in datastore, keyed by username
        user = NintendorksUser(
          parent = None,
          key_name = "key:" + encodedUsername, 
          cookie = storedCookie)
        
        user.put()

        # Write a cookie to the client so they can stay logged in 
        self.response.headers.add_header(
          'set-cookie',
          'NintendorksUsername=%s; expires=Thu, 1-Jan-2015 00:00:01 GMT' % encodedUsername)

        # Once user is logged in, display forum
        self.redirect('/index')
        self.response.out.write(result.headers['set-cookie'])
        
      else:
        # If user did not log in properly, redirect back to login page
        self.redirect('/')
    
    except:
      # Admittedly poor exception handling
      self.response.out.write("<br><br>Unknown error occurred. Try refreshing!")
      logging.error('Exception generated during login')

# Display forum index
class IndexHandler(webapp.RequestHandler):
  def get(self):
    #Retrieve username field from client if it exists
    username = self.request.cookies.get('NintendorksUsername', '')

    #Datastore keyword is username prepended by "key:"
    user = NintendorksUser.get_by_key_name("key:%s" % username)

    if user != None:
      # If user exists in the datastore send cookie data with server request
      newHeader = {"Cookie" : user.cookie}
      result = Utilidorks.fetchretry(self, baseURL, urlfetch.GET, newHeader)

      if None != result:
        # Check that forum cookie is still valid and user is still logged in
        if result.content.find("Logout [") != -1:          
          soup = BeautifulSoup(result.content)
          
          forumLinks = soup.findAll('a', attrs={"class" : re.compile("forumlink")})
          unreadPmText = soup.find('div', attrs={"class" : re.compile("pm-notify-header")})
          
          unreadPmCount = ""
          if unreadPmText != None:
            unreadPmText = unreadPmText.parent
            if len(unreadPmText.contents[1].contents) == 4:
              unreadPmCount = unreadPmText.contents[1].contents[2].string
            else:
              unreadPmCount = unreadPmText.contents[1].contents[0].string
          
          forums = []
          for forum in forumLinks:
            forums.append({
              'id' : forum['href'].split('f=')[1],
              'title' : forum.string
            })
          
          template_values = {
            'forums' : forums,
            'unreadPmCount' : unreadPmCount
          }
                  
          path = os.path.join(os.path.dirname(__file__), 'index.html')
          self.response.out.write(template.render(path, template_values))
          
        else:
          # Clear local cookie if forum login cookie is invalid
          logging.error('Clear: login cookie invalid')
          logging.error(username)
          logging.error(user.cookie)
          logging.error(result.content)
          self.redirect('/clear')                    
    else:
      # Show login page
      logging.error('Clear: user not logged in')
      self.redirect('/clear')

# Reformat forum listing into iPhone-friendly format
class ForumHandler(webapp.RequestHandler):
  def get(self):
    #Retrieve username field from client if it exists
    username = self.request.cookies.get('NintendorksUsername', '')

    #Datastore keyword is username prepended by "key:"
    user = NintendorksUser.get_by_key_name("key:%s" % username)

    if user != None:
      forumURL = ""
      startNumber = ""
      forumID =  self.request.get('f')
      
      if forumID == "newposts":
        forumURL = baseURL + "search.php?search_id=newposts"
      else:
        # Start number of thread list to retrieve
        startNumber = self.request.get('start')
        params = "?f=" + forumID
        params += "&start=" + startNumber
        
        forumURL = baseURL + forumPath + params
      
      # Flag indicating the retrieval failed
      retrievalError = ""

      # If user exists in the datastore send cookie data with server request
      newHeader = {"Cookie" : user.cookie}
      result = Utilidorks.fetchretry(self, forumURL, urlfetch.GET, newHeader)
      
      if None != result:  
        try:
          # Check that forum cookie is still valid and user is still logged in
          if result.content.find("Logout [") != -1:          
            soup = BeautifulSoup(result.content)
              
            threadTitles = soup.findAll('a', attrs={"class" : re.compile("topictitle")})
        
            if forumID == "newposts":
              forumTitle = "New Posts"
            else:
              forumTitle = soup.head.title.contents[0].string[35:]
        
            threads = []
            for thread in threadTitles:
              repliesCell = thread.parent.nextSibling.nextSibling.nextSibling.nextSibling
              postTimeCell = repliesCell.nextSibling.nextSibling.nextSibling.nextSibling
        
              # Check for the existence of an img tag in the first link of the table column;
              # this is the link to the first unread post
              unread = ""
              if hasattr(thread.parent.contents[1].contents[0], "name"):
                unread = "true";
                
              numReplies = int(repliesCell.p.string)
              threads.append({
                'id' : thread['href'].split('t=')[1],
                'lastStart' : (numReplies / postsPerPage) * postsPerPage,
                'title' : thread.string,
                'numReplies' : repliesCell.p.string,
                'lastPostTime' : postTimeCell.p.string,
                'unread': unread
              })
              
            if startNumber == "":
              startNumber = "0"
                
            startInt = int(startNumber)
              
            nextStart = ""
            prevStart = ""
        
            # Determine if previous and/or next page links should be shown;
            # arbitrary stop point because I'm lazy
            if startInt < 3600 and forumID != "newposts":
              nextStart += str(startInt + threadsPerPage)
                
            if startInt >= threadsPerPage:
              prevStart += str(startInt - threadsPerPage)
                
            template_values = {
              'forumID' : forumID,
              'forumTitle' : forumTitle,
              'threads' : threads,
              'retrievalError' : retrievalError,
              'nextStart' : nextStart,
              'prevStart' : prevStart
            }
        
            path = os.path.join(os.path.dirname(__file__), 'forum.html')
            self.response.out.write(template.render(path, template_values))
          
          else:
            # Clear local cookie if forum login cookie is invalid
            logging.error('Clear: login cookie invalid')
            logging.error(username)
            logging.error(user.cookie)            
            self.redirect('/clear')
            
        except:
          # Try again
          self.redirect('/forum?f=' + self.request.get('f'))
    else:
      # Redirect to login page if user doesn't have login cookie
      logging.error('Clear: user not logged in')      
      self.redirect('/clear')
    
# Reformat thread into iPhone-friendly format
class ThreadHandler(webapp.RequestHandler):
  # Handle thread GET
  def get(self): 
    # Retrieve username field from client if it exists
    username = self.request.cookies.get('NintendorksUsername', '')

    # Datastore keyword is username prepended by "key:"
    user = NintendorksUser.get_by_key_name("key:%s" % username)
    
    self.showThread(
      user, self.request.get('id'), self.request.get('forumID'), self.request.get('start'), 
      self.request.get('numReplies'), self.request.get('unread'), self.request.get('bottom'))      
        
  def showThread(self, user, id, forumID, start, numReplies, unread, bottom):
    debugText = ""
    if user != None:
      # Page number and ID of thread list to retrieve
      params = "?&t=" + id
      if "" == unread:
        params += "&start=" + start
      else:
        params += "&view=unread"
      
      # Flag to indicate whether the retrieval failed
      retrievalError = ""

      # If user exists in the datastore send cookie data with server request
      newHeader = {"Cookie" : user.cookie}
      threadURL = baseURL + threadPath + params
      result = Utilidorks.fetchretry(self, threadURL, urlfetch.GET, newHeader)

      if None != result:
        # Check that forum cookie is still valid and user is still logged in
        if result.content.find("Logout [") != -1:
          try:       
            # Parse the page
            soup = BeautifulSoup(result.content)
            postContents = soup.findAll('div', attrs={"class" : "postbody"})
            postUsers = soup.findAll('div', attrs={"class" : "postauthor"})
            threadTitle = soup.head.title.contents[0].string[35:]
            
            # Parameters needed for replying
            topic_cur_post_id_txt = ""
            topic_cur_post_id = soup.find('input', attrs={"name" : "topic_cur_post_id"})
            if None != topic_cur_post_id:
              topic_cur_post_id_txt = topic_cur_post_id['value']
              
            form_token_txt = ""
            form_token = soup.find('input', attrs={"name" : "form_token"})
            if None != form_token:
              form_token_txt = form_token['value']
            
            # Find forumID if coming from New Posts index
            if forumID == "newposts":
              forumID = form_token.parent['action'][27:28]
            
            posts = []
            i = 0
      
            # Post ID of first unread post, if requested
            unreadPost = ""
            
            # Prepare posts for display
            # Take time field after the &nbsp;
            for post in postContents:
              # Check for avatar
              avatarURL = ""
              avatarElement = postUsers[i].nextSibling.nextSibling.nextSibling 
              if hasattr(avatarElement, "contents"):
                avatarURL = baseURL + avatarElement.contents[0]['src'][2:]
                
              # Post ID
              postID = postUsers[i].previousSibling.previousSibling
                
              # Check for unread indicator
              if "true" == unread:
                if hasattr(postID.previousSibling, "name") and "a" == postID.previousSibling.name:
                  unreadPost = postID['name']
                  
              # Check for edit icon
              editable = ""
              if None != post.parent.contents[1].a:
                if None != post.parent.contents[1].contents[0].nextSibling.nextSibling:
                  editable = "true"
              
              # Look for spoiler tags; make them clickable to view contents
              postText = post.renderContents().replace(
                'font-size: 10px; color: #000000; background-color: #000000\"', 
                'color: #000000; background-color: #000000\" onClick=\"javascript: this.style.color = \'#fff\';\"')
      
              posts.append({
                 "id"       : postID['name'],
                 "text"     : postText,
                 "user"     : postUsers[i].string,
                 "time"     : post.previousSibling.previousSibling.contents[1].string[6:],
                 "avatar"   : avatarURL,
                 "editable" : editable
              })
               
              i = i + 1
      
            prevStart = ""
            nextStart = ""
            numReplies = int(numReplies)
      
            if "" == start:
              if "" == unread:
                start = "0"
              else:
                # Compute start using the previous page, if any
                prevString = soup.find(text="Previous")
                if None != prevString and "a" == prevString.parent.name:
                  start = re.search('(?<=start\=)\w+', prevString.parent['href']).group(0)
                  tempNum = int(start) + postsPerPage
                  start = str(tempNum)
                else:
                  start = "0"
                      
            startNum = int(start)
      
            # Determine if previous and/or next page links should be shown
            nextPage = ""
            prevPage = ""
            if (startNum + postsPerPage) <= numReplies:
              nextStart = startNum + postsPerPage
              
            if startNum >= postsPerPage:
              prevStart = startNum - postsPerPage
              
            # Compute current page and total number of pages
            totalPages = (numReplies / postsPerPage) + 1
            currentPage = (startNum / postsPerPage) + 1
            
            template_values = {
              'forumID' : forumID,
              'threadID' : id,
              'threadTitle' : threadTitle,
              'posts' : posts,
              'retrieveError' : retrievalError,
              'unreadPost' : unreadPost,
              'start' : start,
              'nextStart' : nextStart,
              'prevStart' : prevStart,
              'numReplies' : numReplies,
              'totalPages' : totalPages,
              'currentPage' : currentPage,
              'topic_cur_post_id' : topic_cur_post_id_txt,
              'lastclick' : topic_cur_post_id.nextSibling['value'],
              'creation_time' : topic_cur_post_id.nextSibling['value'],
              'form_token' : form_token_txt,
              'bottom' : bottom,
              'debugText' : debugText
            }
      
            path = os.path.join(os.path.dirname(__file__), 'thread.html')
            self.response.out.write(template.render(path, template_values))
            
          except:
            # Log the exception
            cla, exc, trbk = sys.exc_info()
            excName = cla.__name__
            try:
              excArgs = exc.__dict__["args"]
            except KeyError:
              excArgs = "<no args>"
            excTb = traceback.format_tb(trbk, 5)
            logging.error(excName)
            logging.error(result.content) 
             
            # Display error and stop
            template_values = { 'errorMsg' : 
            "I have no idea what happened, but try refreshing!"}
            path = os.path.join(os.path.dirname(__file__), 'error.html')
            self.response.out.write(template.render(path, template_values))
        else:
          # Clear local cookie if forum login cookie is invalid
          logging.error('Clear: login cookie invalid')
          logging.error(user.cookie)
          logging.error(result.content)
          self.redirect('/clear')

# Handle posting actions
class PostHandler(webapp.RequestHandler):
  # Handle thread GET
  def get(self): 
    # Retrieve username field from client if it exists
    username = self.request.cookies.get('NintendorksUsername', '')

    # Datastore keyword is username prepended by "key:"
    user = NintendorksUser.get_by_key_name("key:%s" % username)

    if user != None:
      mode = self.request.get('mode')
      postPath = "posting.php?mode=" + mode

      # Assemble URL to retrieve the post page
      params = "&f=" + self.request.get('f')
      
      # Only parse postID if editing or quoting
      postID = ""
      if "edit" == mode or "quote" == mode:
        postID = self.request.get('id')[1:]
        params += "&p=" + postID

      # Add thread ID if replying
      if "reply" == mode:
        params += "&t=" + self.request.get('t')

      postURL = baseURL + postPath + params
      
      # Flag indicating the retrieval failed
      retrievalError = ""

      # If user exists in the datastore send cookie data with server request
      newHeader = {"Cookie" : user.cookie}
      result = Utilidorks.fetchretry(self, postURL, urlfetch.GET, newHeader)
      
      if None != result:        
        # Parse the page
        soup = BeautifulSoup(result.content)
        
        # Parameters needed for replying
        lastclick = soup.find('input', attrs={"name" : "lastclick"})
        creation_time = soup.find('input', attrs={"name" : "creation_time"})
        form_token = soup.find('input', attrs={"name" : "form_token"})
  
        # Only get message contents if editing or quoting
        messageText = ""
        if "edit" == mode or "quote" == mode:  
          postContents = soup.find('textarea', attrs={"name" : "message"}).string
          if None != postContents:
            messageText = postContents.string
        
        # Look for topic_cur_post_id when quoting or replying
        topic_cur_post_id = ""
        if "quote" == mode or "reply" == mode:
          curPostTemp = soup.find('input', attrs={"name" : "topic_cur_post_id"})
          if None != curPostTemp:
            topic_cur_post_id = curPostTemp['value']
            
        # Don't search for title when posting new thread
        threadText = ""
        if "post" != mode:
          threadTitle = soup.find('a', attrs={"class" : "titles"})
          if None != threadTitle:
            threadText = threadTitle.string
  
        # Only search for the subject for edit, quote and reply (i.e., not post)
        subjectText = ""
        if "post" != mode:
          subjectContents = soup.find('input', attrs={"name" : "subject"})
          if None != subjectContents:
            subjectText = subjectContents['value']
  
        template_values = {
          'mode' : mode,
          'postID' : postID,
          'forumID' : self.request.get('f'),
          'threadID' : self.request.get('t'),
          'threadTitle' : threadText,
          'subjectContents' : subjectText,
          'postContents' : messageText,
          'start' : self.request.get('start'),
          'numReplies' : self.request.get('numReplies'),
          'lastclick' : lastclick['value'],
          'creation_time' : creation_time['value'],
          'form_token' : form_token['value'],
          'topic_cur_post_id' : topic_cur_post_id
        }
                    
        path = os.path.join(os.path.dirname(__file__), 'edit.html')
        self.response.out.write(template.render(path, template_values))

  # Handle edit POST
  def post(self):
    # Check input parameters
    mode = self.request.get('mode')
    if "edit" != mode and "quote" != mode and "reply" != mode and "post" != mode:
      sys.exit()
    
    # Retrieve username field from client if it exists
    username = self.request.cookies.get('NintendorksUsername', '')

    # Datastore keyword is username prepended by "key:"
    user = NintendorksUser.get_by_key_name("key:%s" % username)

    if user != None:
      sidIndex = user.cookie.index("sid=") + 4
      sid = user.cookie[sidIndex:user.cookie.index(";",sidIndex)]
      
      # Set up POST parameters in dictionary form for posting reply
      params = {'mode' : mode,
                'sid' : sid,
                'f' : self.request.get('forumID'),
                't' : self.request.get('threadID'),
                'p' : self.request.get('postID'),
                'subject' : self.request.get('subject').encode("utf-8"),
                'message' : self.request.get('message').encode("utf-8"),
                'lastclick' : self.request.get('lastclick'),
                'creation_time' : self.request.get('creation_time'),
                'form_token' : self.request.get('form_token'),
                'topic_cur_post_id' : self.request.get('topic_cur_post_id'),
                'post' : "Submit"}
    
      # URL-encode parameters dictionary      
      encodedParams = urllib.urlencode(params)
    
      try:
        numRepliesStr = self.request.get('numReplies')
        if "" == numRepliesStr:
          numRepliesStr = "0"
        
        # Send login parameters straight to reply script
        result = urlfetch.fetch(
          url = baseURL + replyPath,
          payload = encodedParams, method=urlfetch.POST, follow_redirects=False, deadline=10)

        # Check if post was successful
        posted = False
        if result.content.find("success") != -1:
          logging.debug(mode + ' performed successfully')
          
          posted = True
          
        elif ("quote" == mode or "reply" == mode) and result.content.find("one new post has been made") != -1:
          # Update topic_cur_post_id and try posting again; the chances of 
          # someone posting again before this code can resubmit is pretty small
          soup = BeautifulSoup(result.content)
          topic_cur_post_id = soup.find('input', attrs={"name" : "topic_cur_post_id"})
          params["topic_cur_post_id"] = topic_cur_post_id['value']
          encodedParams = urllib.urlencode(params)
          result = urlfetch.fetch(
            url = baseURL + replyPath,
            payload = encodedParams, method=urlfetch.POST, follow_redirects=False, deadline=10)
          posted = True
          
          logging.debug('Resubmitting post')

        else:
          logging.error(mode + ' failed!')
          
          # Find error message and display it
          soup = BeautifulSoup(result.content)
          errorMsg = soup.find('span', attrs={"class" : "genmed error"})          
          logging.error(errorMsg.string)
                    
          # Display error
          template_values = { 'errorMsg' : errorMsg.string  + \
                             "<br>Hit the back button and try again!"}
          path = os.path.join(os.path.dirname(__file__), 'error.html')
          self.response.out.write(template.render(path, template_values))   
                  
        if posted:
          # If successful quote or reply increment numReplies by 1           
          if "quote" == mode or "reply" == mode:
            numRepliesInt = int(numRepliesStr) + 1
            numRepliesStr = str(numRepliesInt)
              
          # Redisplay thread
          newURL = ""
          if "post" != mode:
            newURL = '/thread?id=' + self.request.get('threadID') \
              + '&forumID=' + self.request.get('forumID') \
              + '&numReplies=' + numRepliesStr \
              + '&start=' + self.request.get('start')
          else:
            # For a new thread, just show the forum
            newURL = '/forum?f=' + self.request.get('forumID')
                        
          # Jump to the bottom for quote and reply
          if "quote" == mode or "reply" == mode:
            newURL += '&bottom=true'
          
          # Go to the edited post for edit
          if "edit" == mode:
            newURL += '&bottom=ignore' + '#p' + self.request.get('postID')
          
          self.redirect(newURL)
    
      except:
        errorMsg = "Unknown error occurred! "
        if "post" == mode:
          errorMsg = errorMsg + "Hit the back button and try again."
          
        # Display error
        template_values = { 'errorMsg' : errorMsg }
        path = os.path.join(os.path.dirname(__file__), 'error.html')
        self.response.out.write(template.render(path, template_values))          
        
        # Log the cause
        cla, exc, trbk = sys.exc_info()
        excName = cla.__name__
        try:
          excArgs = exc.__dict__["args"]
        except KeyError:
          excArgs = "<no args>"
        excTb = traceback.format_tb(trbk, 5)
        logging.error(excName)
        logging.error(excArgs)
        logging.error(excTb)

# Parse private message folders
class PMHandler(webapp.RequestHandler):
  def get(self):
    if self.request.get('show') == "inbox":
      self.showInbox()
    elif self.request.get('show') == "message":
      self.showMessage(id)

  def showInbox(self):
    #Retrieve username field from client if it exists
    username = self.request.cookies.get('NintendorksUsername', '')

    #Datastore keyword is username prepended by "key:"
    user = NintendorksUser.get_by_key_name("key:%s" % username)

    if user != None:
      forumURL = ""
      startNumber = ""
      folderID = self.request.get('show')
      
      pmURL = baseURL + pmPath + "&folder=" + folderID
      
      # Flag indicating the retrieval failed
      retrievalError = ""

      # If user exists in the datastore send cookie data with server request
      newHeader = {"Cookie" : user.cookie}
      result = Utilidorks.fetchretry(self, pmURL, urlfetch.GET, newHeader)

      if None != result:
        try:
          # Check that forum cookie is still valid and user is still logged in
          if result.content.find("Logout [") != -1:          
            soup = BeautifulSoup(result.content)
              
            threadTitles = soup.findAll('span', attrs={"class" : re.compile("topictitle")})
            threadAuthors = soup.findAll('p', attrs={"class" : re.compile("topicauthor")})
            threadTimes = soup.findAll('p', attrs={"class" : re.compile("topicdetails")})
        
            if folderID == "inbox":
              forumTitle = "Inbox"
            else:
              forumTitle = "Private Messages"
        
            i = 0
            threads = []
            for thread in threadTitles:
              # Check for the existence of an img tag in the first link of the table column;
              # this is the link to the first unread post
              unread = ""
              if thread.parent.previousSibling.previousSibling.previousSibling.previousSibling.img['alt'] == "New messages":
                unread = "true";
                
              threads.append({
                'id' : thread.a['href'].split('p=')[1],
                'lastStart' : "",
                'title' : thread.a.string,
                'time' : threadTimes[i*2].string,
                'author' : threadAuthors[i].a.string,
                'unread': unread
              })
              i = i + 1
              
            if startNumber == "":
              startNumber = "0"
                
            startInt = int(startNumber)
              
            nextStart = ""
            prevStart = ""
        
            # Determine if previous and/or next page links should be shown;
            # arbitrary stop point because I'm lazy
            if startInt < 3600:
              nextStart += str(startInt + threadsPerPage)
                
            if startInt >= threadsPerPage:
              prevStart += str(startInt - threadsPerPage)
                
            template_values = {
              'forumID' : folderID,
              'forumTitle' : forumTitle,
              'threads' : threads,
              'retrievalError' : retrievalError,
              'nextStart' : nextStart,
              'prevStart' : prevStart
            }
        
            path = os.path.join(os.path.dirname(__file__), 'pmlisting.html')
            self.response.out.write(template.render(path, template_values))
          
          else:
            # Clear local cookie if forum login cookie is invalid
            logging.error('Clear: login cookie invalid')            
            self.redirect('/clear')
            
        except:
          # Try again
          #self.redirect('/forum?f=' + self.request.get('f'))
          sys.exit()

    else:
      # Redirect to login page if user doesn't have login cookie
      logging.error('Clear: user not logged in')      
      self.redirect('/clear')
      
  def showMessage(self, id):
    #Retrieve username field from client if it exists
    username = self.request.cookies.get('NintendorksUsername', '')
  
    #Datastore keyword is username prepended by "key:"
    user = NintendorksUser.get_by_key_name("key:%s" % username)

    if user != None:      
      # Construct path to message
      messageURL = baseURL + pmPath + "&mode=view&p=" + self.request.get('id')
      
      # Flag to indicate whether the retrieval failed
      retrievalError = ""

      # If user exists in the datastore send cookie data with server request
      newHeader = {"Cookie" : user.cookie}
      result = Utilidorks.fetchretry(self, messageURL, urlfetch.GET, newHeader)

      if None != result:
        # Parse the page
        soup = BeautifulSoup(result.content)
        postContents = soup.find('div', attrs={"class" : "postbody"})
        postInfo = soup.findAll('td', attrs={"class" : "row1 gen"})
        
        template_values = {
          'postText' : postContents.renderContents(),
          'postSubject' : postInfo[0].string,
          'postAuthor' : postInfo[1].a.string,
          'postTime' : postInfo[2].string
        }
        
        path = os.path.join(os.path.dirname(__file__), 'pm.html')
        self.response.out.write(template.render(path, template_values))
            
# Clear cookie from client
class CookieCleaner(webapp.RequestHandler):
  def get(self):
    self.response.headers.add_header(
      'set-cookie',
      'NintendorksUsername=')

    self.redirect('/')

# Utility methods
class Utilidorks():
  @staticmethod
  def fetchretry(requestHandler, newUrl, newMethod, newHeaders):
    # Exponential backoff start timeout
    backoff = 2
    retries = 3
    
    success = False
    result = None
    for i in range(1,retries+1):
      try: 
        result = urlfetch.fetch(
          url = newUrl,
          method = newMethod,
          headers = newHeaders,
          deadline = backoff)
        
        success = True
        break
  
      except:
        logging.debug('DownloadError; retrying')
        backoff = backoff * 2
        
        # Log the cause if all retries fail
        if i == retries:
          cla, exc, trbk = sys.exc_info()
          excName = cla.__name__
          try:
            excArgs = exc.__dict__["args"]
          except KeyError:
            excArgs = "<no args>"
          excTb = traceback.format_tb(trbk, 5)
          logging.error(excName)
          logging.error(excArgs)
          logging.error(excTb)          

    if False == success:
      # Display error
      template_values = { 'errorMsg' : 
      "I don't know why the hell the Nintendorks server isn't responding, but try refreshing!"}
      path = os.path.join(os.path.dirname(__file__), 'error.html')
      requestHandler.response.out.write(template.render(path, template_values))
      
    return result

# Set up path handlers
application = webapp.WSGIApplication(
  [('/', MainPage),
   ('/login', LoginHandler),
   ('/index', IndexHandler),
   ('/forum', ForumHandler),
   ('/thread', ThreadHandler),
   ('/post', PostHandler),
   ('/pm', PMHandler),
   ('/clear', CookieCleaner)],
  debug=True)

def profile_main():
 # This is the main function for profiling 
 # We've renamed our original main() above to real_main()
 import cProfile, pstats, StringIO
 prof = cProfile.Profile()
 prof = prof.runctx("real_main()", globals(), locals())
 stream = StringIO.StringIO()
 stats = pstats.Stats(prof, stream=stream)
 stats.sort_stats("time")  # Or cumulative
 stats.print_stats(80)  # 80 = how many to print
 # The rest is optional.
 # stats.print_callees()
 # stats.print_callers()
 logging.info("Profile data:\n%s", stream.getvalue())
 
def real_main():
  run_wsgi_app(application)

main = real_main

if __name__ == "__main__":
  main()
